#Region "Imports"
Imports System.IO
Imports System.Collections.Generic
#End Region
''' <summary>
''' Main class for writing PDF file.
''' This is based on the PDF standard version 1.7.''' </summary>
''' <remarks></remarks>
Friend Class ClsPdfWriter
#Region "Private members"
    Private _tmpFileName As String
    Private _binWriter As BinaryWriter  'A binary writer is used because PDF often contain binary data
    Private _nextFreeObjectNumber As Long   'The next number to use when adding new objects to the pdf
    Private _objectAllocatedCount As Long   'Number of new object allocated
    Private _objectsWritten As New List(Of clsPdfBaseObject) 'New or updated objects written in the PDF file
    Private _updateXrefPosition As Long 'Abosulte position of the cross-reference table for new or updated objects
    Private _documentInfo As ClsPdfDocumentInformation 'The info object of the PDF which is always modified because we change the modification date
#End Region
#Region "Constructors"
    ''' <summary>
    ''' This constructor is used when we need to update en existing PDF file
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New(buffer As Byte(), ByVal nextFreeObject As Long)
        'A PDF file is updated, we save the changes in a temporary file
        _tmpFileName = Path.GetTempFileName()
        Dim objFS As New FileStream(_tmpFileName, FileMode.Append, FileAccess.Write)
        _binWriter = New BinaryWriter(objFS)
        _nextFreeObjectNumber = nextFreeObject
        _objectAllocatedCount = 0
        'We copy the whole content of the PDF to be updated
        _copyToStream(buffer)
    End Sub
#End Region
#Region "Public Properties"
    Public ReadOnly Property FileName() As String
        Get
            FileName = _tmpFileName
        End Get
    End Property
#End Region
#Region "Public Methods"
    Public Sub WriteXRefStream(ByVal lastXrefPosition As Long, ByVal xrefCount As Integer, ByRef currentTrailer As clsPdfDictionnary, docInfo As ClsPdfDocumentInformation)
        Dim i, count As Integer
        Dim firstObjectNumber As Long
        Dim wfield As Byte()
        Dim objStream As clsPdfBaseObject
        Dim dict As New clsPdfDictionnary
        Dim obj As clsPdfBaseObject
        Dim objArray As New List(Of clsPdfBaseObject)

        If _objectsWritten.Count > 0 Then

            _updateXrefPosition = _binWriter.BaseStream.Position

            objStream = CreateStreamObject()
            objStream.ObjectNumber = AllocateObjectNumber()

            _objectsWritten.Sort()

            'We seek contiguous sequence of object numbers
            i = 0
            count = 0
            firstObjectNumber = _objectsWritten(i).ObjectNumber
            While i < _objectsWritten.Count
                If (i + 1) < _objectsWritten.Count Then
                    If _objectsWritten(i + 1).ObjectNumber - _objectsWritten(i).ObjectNumber = 1 Then
                        i += 1
                        count += 1
                    Else
                        obj = CreateNumericObject(firstObjectNumber)
                        objArray.Add(obj)
                        obj = CreateNumericObject(count + 1)
                        objArray.Add(obj)
                        i += 1
                        count = 0
                        firstObjectNumber = _objectsWritten(i).ObjectNumber
                    End If
                Else
                    obj = CreateNumericObject(firstObjectNumber)
                    objArray.Add(obj)
                    obj = CreateNumericObject(count + 1)
                    objArray.Add(obj)
                    Exit While
                End If
            End While

            obj = CreateArrayObject(objArray)
            dict.Add(PDF_INDEX, obj)

            obj = CreateNameObject(PDF_XREF_STREAM)
            dict.Add(PDF_TYPE, obj)

            obj = CreateNumericObject(_objectsWritten(_objectsWritten.Count - 1).ObjectNumber + 1)
            dict.Add(PDF_SIZE, obj)

            obj = CreateNumericObject(lastXrefPosition)
            dict.Add(PDF_PREV, obj)

            obj = currentTrailer.Item(PDF_ROOT)
            dict.Add(PDF_ROOT, obj)

            obj = CreateIndirectReferenceObject(docInfo.ObjectNumber, docInfo.GenerationNumber)
            dict.Add(PDF_INFO, obj)


            'Create the W entry
            objArray.Clear()
            obj = CreateNumericObject(1)
            objArray.Add(obj)
            obj = CreateNumericObject(4)
            objArray.Add(obj)
            obj = CreateNumericObject(1)
            objArray.Add(obj)
            obj = CreateArrayObject(objArray)
            dict.Add(PDF_W, obj)

            For Each obj In _objectsWritten
                objStream.AddValue(CByte(1))
                wfield = BitConverter.GetBytes(CInt(obj.Position))
                For i = wfield.Length - 1 To 0 Step -1
                    objStream.AddValue(wfield(i))
                Next
                objStream.AddValue(CByte(0))
            Next

            obj = CreateNumericObject(objStream.Count + 1)
            dict.Add(PDF_LENGTH, obj)

            obj = _beginWriteIndirectObject(objStream.ObjectNumber, 0)
            _writeDictionnary(dict)
            _write(objStream)
            _endWriteIndirectObject(obj)

            _write(ControlChars.CrLf)
            _write(PDF_STARTXREF)
            _write(ControlChars.CrLf)
            _write(_updateXrefPosition.ToString)
            _write(ControlChars.CrLf)
            _write(PDF_EOF)

        End If
    End Sub

    Public Sub WriteXRef()
        Dim i, start As Integer
        If _objectsWritten.Count > 0 Then

            _objectsWritten.Sort()

            _updateXrefPosition = _binWriter.BaseStream.Position
            _write(PDF_XREF)
            _write(ControlChars.CrLf)

            'We seek contiguous sequence of object numbers
            i = 0
            start = 0
            While i < _objectsWritten.Count
                If (i + 1) < _objectsWritten.Count Then
                    If _objectsWritten(i + 1).ObjectNumber - _objectsWritten(i).ObjectNumber = 1 Then
                        i += 1
                    Else
                        _writeXRef(start, i)
                        i += 1
                        start = i
                    End If
                Else
                    _writeXRef(start, i)
                    Exit While
                End If
            End While

        End If
    End Sub
    ''' <summary>
    ''' Write the trailer
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub WriteTrailer(ByVal lastXrefPosition As Long, ByVal xrefCount As Integer, ByRef currentTrailer As clsPdfDictionnary)
        'Dim dict As New clsPdfDictionnary
        'Dim rootObj As clsPdfBaseObject

        _write(PDF_TRAILER)
        _write(ControlChars.CrLf)

        'currentTrailer.Add(PDF_SIZE, CreateNumericObject(_objectsWritten.Count + xrefCount), True)
        currentTrailer.Add(PDF_SIZE, CreateNumericObject(xrefCount + _objectAllocatedCount), True)
        currentTrailer.Add(PDF_PREV, CreateNumericObject(lastXrefPosition), True)
        'dict.Add(PDF_INFO, CreateIndirectReferenceObject(_documentInfo.PdfObject.ObjectNumber, _documentInfo.PdfObject.GenerationNumber))

        'rootObj = currentTrailer.Item(PDF_ROOT)
        'dict.Add(PDF_ROOT, rootObj)

        _writeDictionnary(currentTrailer)
        _write(ControlChars.CrLf)
        _write(PDF_STARTXREF)
        _write(ControlChars.CrLf)
        _write(_updateXrefPosition.ToString)
        _write(ControlChars.CrLf)
        _write(PDF_EOF)
    End Sub
    ''' <summary>
    ''' Write an image
    ''' </summary>
    ''' <param name="image"></param>
    ''' <remarks></remarks>
    Public Sub Write(ByVal image As clsPDFImage, ByVal objectNumber As Long)
        Dim indirectObject As clsPdfBaseObject
        SyncLock image
            indirectObject = _beginWriteIndirectObject(objectNumber, 0)
            _writeDictionnary(image.Dictionnary)
            _write(image.Stream.StreamObject)
            _endWriteIndirectObject(indirectObject)
        End SyncLock
    End Sub
    ''' <summary>
    ''' Write a page information object
    ''' </summary>
    ''' <param name="pageInformation"></param>
    ''' <remarks></remarks>
    Public Sub Write(ByVal pageInformation As clsPDFPage)
        Dim indirectObject As clsPdfBaseObject
        indirectObject = _beginWriteIndirectObject(pageInformation.ObjectNumber, pageInformation.GenerationNumber)
        _writeDictionnary(pageInformation)
        _endWriteIndirectObject(indirectObject)
    End Sub
    ''' <summary>
    ''' Write the document information object
    ''' </summary>
    ''' <param name="documentInfo"></param>
    ''' <remarks></remarks>
    Public Sub Write(ByVal documentInfo As ClsPdfDocumentInformation)
        Dim indirectObject As clsPdfBaseObject
        _documentInfo = documentInfo
        indirectObject = _beginWriteIndirectObject(documentInfo.ObjectNumber, documentInfo.GenerationNumber)
        _writeDictionnary(documentInfo)
        _endWriteIndirectObject(indirectObject)
    End Sub
    ''' <summary>
    ''' Write the viewer preferences
    ''' </summary>
    ''' <param name="viewerPreferences"></param>
    ''' <remarks></remarks>
    Public Sub Write(ByVal viewerPreferences As clsPDFViewerPreferences)
        Dim indirectObject As clsPdfBaseObject

        indirectObject = _beginWriteIndirectObject(viewerPreferences.ObjectNumber, viewerPreferences.GenerationNumber)
        _writeDictionnary(viewerPreferences)
        _endWriteIndirectObject(indirectObject)

    End Sub
    ''' <summary>
    ''' Write a dictionnary object
    ''' </summary>
    ''' <param name="dict"></param>
    ''' <remarks></remarks>
    Public Sub WriteDictionnary(ByVal dict As clsPdfDictionnary)
        Dim indirectObject As clsPdfBaseObject

        indirectObject = _beginWriteIndirectObject(dict.ObjectNumber, dict.GenerationNumber)
        _writeDictionnary(dict)
        _endWriteIndirectObject(indirectObject)

    End Sub
    ''' <summary>
    ''' Write an annotation object
    ''' </summary>
    ''' <param name="annotation"></param>
    ''' <param name="objectNumber"></param>
    ''' <remarks></remarks>
    Public Sub Write(ByVal annotation As ClsPdfAnnotation, ByVal objectNumber As Long)
        Dim indirectObject As clsPdfBaseObject
        indirectObject = _beginWriteIndirectObject(objectNumber, 0)
        _writeDictionnary(annotation)
        _endWriteIndirectObject(indirectObject)
    End Sub
    ''' <summary>
    ''' Write a stream object
    ''' </summary>
    ''' <param name="stream"></param>
    ''' <param name="objectNumber"></param>
    ''' <remarks></remarks>
    Public Sub Write(ByVal stream As clsPdfStreamObject, ByVal objectNumber As Long)
        Dim indirectObject As clsPdfBaseObject
        indirectObject = _beginWriteIndirectObject(objectNumber, 0)
        _writeDictionnary(stream.Dictionnary)
        _write(stream.StreamObject)
        _endWriteIndirectObject(indirectObject)
    End Sub
    ''' <summary>
    ''' To close the binary writer
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Close()
        _binWriter.Close()
    End Sub
#End Region
#Region "Friend Methods"
    ''' <summary>
    ''' We allocate a new free object number
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Friend Function AllocateObjectNumber() As Long
        AllocateObjectNumber = _nextFreeObjectNumber
        _nextFreeObjectNumber += 1
        _objectAllocatedCount += 1
    End Function
#End Region
#Region "Private Methods"
    ''' <summary>
    ''' Copy the entire content of the PDF to another byte stream
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub _copyToStream(buffer As Byte())
        _binWriter.Write(Buffer)
        'Dim b() As Byte
        'binReader.BaseStream.Position = 0
        'b = binReader.ReadBytes(2048)
        'While b.Length > 0
        '    _binWriter.Write(b)
        '    b = binReader.ReadBytes(2048)
        'End While
    End Sub
    ''' <summary>
    ''' Write a string
    ''' </summary>
    ''' <param name="s"></param>
    ''' <remarks></remarks>
    Private Sub _write(ByVal s As String)
        Dim c As Char
        For Each c In s
            _binWriter.Write(c)
        Next
    End Sub
    ''' <summary>
    ''' Write a dictionnary
    ''' </summary>
    ''' <param name="dictionnary"></param>
    ''' <remarks></remarks>
    Private Sub _writeDictionnary(ByVal dictionnary As clsPdfDictionnary)
        Dim key As String
        Dim obj As clsPdfBaseObject
        _write("<<")
        For Each key In dictionnary.Hashtable.Keys
            _write("/")
            _write(key)
            _write(" "c)
            obj = dictionnary.Item(key)
            _write(obj)
        Next
        _write(">>")
    End Sub
    ''' <summary>
    ''' Write a literal string
    ''' </summary>
    ''' <param name="literalStringObj"></param>
    ''' <remarks></remarks>
    Private Sub _writeLiteralString(ByVal literalStringObj As clsPdfBaseObject)
        _write("("c)
        If Not literalStringObj.Value Is Nothing Then
            _binWriter.Write(literalStringObj.Value)
        End If
        _write(")"c)
    End Sub
    ''' <summary>
    ''' Write a numeric object
    ''' </summary>
    ''' <param name="numericObj"></param>
    ''' <remarks></remarks>
    Private Sub _writeNumericObject(ByVal numericObj As clsPdfBaseObject)
        _write(" "c)
        _binWriter.Write(numericObj.Value)
        _write(" "c)
    End Sub
    ''' <summary>
    ''' Write an indirect reference object
    ''' </summary>
    ''' <param name="indirectReferenceObj"></param>
    ''' <remarks></remarks>
    Private Sub _writeIndirectReferenceObject(ByVal indirectReferenceObj As clsPdfBaseObject)
        _write(indirectReferenceObj.ObjectNumber.ToString)
        _write(" "c)
        _write(indirectReferenceObj.GenerationNumber.ToString)
        _write(" R")
        _write(" "c)
    End Sub
    ''' <summary>
    ''' Write a name object
    ''' </summary>
    ''' <param name="nameObject"></param>
    ''' <remarks></remarks>
    Private Sub _writeNameObject(ByVal nameObject As clsPdfBaseObject)
        _write(" /")
        _write(nameObject.GetString)
    End Sub
    ''' <summary>
    ''' Write a boolean object
    ''' </summary>
    ''' <param name="booleanObject"></param>
    ''' <remarks></remarks>
    Private Sub _writeBooleanObject(ByVal booleanObject As clsPdfBaseObject)
        Select Case booleanObject.SubType
            Case PdfObjectSubTypes.TrueObject
                _write(" "c)
                _write(PDF_TRUE)
            Case PdfObjectSubTypes.FalseObject
                _write(" "c)
                _write(PDF_FALSE)
        End Select
    End Sub
    ''' <summary>
    ''' Write an array object
    ''' </summary>
    ''' <param name="arrayObject"></param>
    ''' <remarks></remarks>
    Private Sub _writeArrayObject(ByVal arrayObject As clsPdfBaseObject)
        Dim arrayList As List(Of clsPdfBaseObject)
        Dim obj As clsPdfBaseObject
        arrayList = arrayObject.GetArray
        _write("["c)
        If Not arrayList Is Nothing Then
            For Each obj In arrayList
                _write(obj)
            Next
        End If
        _write("]"c)
    End Sub
    ''' <summary>
    ''' Write an hexadecimal string object
    ''' </summary>
    ''' <param name="hexadecimalStringObject"></param>
    ''' <remarks></remarks>
    Private Sub _writeHexadecimalStringObject(ByVal hexadecimalStringObject As clsPdfBaseObject)
        _write("<"c)
        If Not hexadecimalStringObject.Value Is Nothing Then
            _binWriter.Write(hexadecimalStringObject.Value)
        End If
        _write(">"c)
    End Sub
    Private Sub _writeStreamObject(ByVal streamObject As clsPdfBaseObject)
        _write(ControlChars.CrLf)
        _write(PDF_STREAM)
        _write(ControlChars.CrLf)
        _binWriter.Write(streamObject.Value)
        _write(ControlChars.CrLf)
        _write(PDF_ENDSTREAM)
        _write(ControlChars.CrLf)
    End Sub
    ''' <summary>
    ''' Write the content of a pdf object
    ''' </summary>
    ''' <param name="obj"></param>
    ''' <remarks></remarks>
    Private Sub _write(ByVal obj As clsPdfBaseObject)
        Select Case obj.Type
            Case PdfObjectTypes.StringObject
                Select Case obj.SubType
                    Case PdfObjectSubTypes.LiteralStringObject
                        _writeLiteralString(obj)
                    Case PdfObjectSubTypes.HexadecimalStringObject
                        _writeHexadecimalStringObject(obj)
                End Select
            Case PdfObjectTypes.NumericObject
                _writeNumericObject(obj)
            Case PdfObjectTypes.IndirectReferenceObject
                _writeIndirectReferenceObject(obj)
            Case PdfObjectTypes.NameObject
                _writeNameObject(obj)
            Case PdfObjectTypes.ArrayObject
                _writeArrayObject(obj)
            Case PdfObjectTypes.DictionaryObject
                _writeDictionnary(obj.GetPdfDictionnary)
            Case PdfObjectTypes.StreamObject
                _writeStreamObject(obj)
            Case PdfObjectTypes.BooleanObject
                _writeBooleanObject(obj)
        End Select
    End Sub
    ''' <summary>
    ''' Begin to write an indirect object
    ''' </summary>
    ''' <param name="objectNumber"></param>
    ''' <param name="generationNumber"></param>
    ''' <remarks></remarks>
    Private Function _beginWriteIndirectObject(ByVal objectNumber As Long, ByVal generationNumber As Long) As clsPdfBaseObject
        Dim indirectObject As clsPdfBaseObject = CreateIndirectObject(objectNumber, generationNumber)
        indirectObject.Position = _binWriter.BaseStream.Position

        _write(objectNumber.ToString)
        _write(" "c)
        _write(generationNumber.ToString)
        _write(" "c)
        _write(PDF_OBJ)
        _write(ControlChars.CrLf)

        _beginWriteIndirectObject = indirectObject
    End Function
    ''' <summary>
    ''' Write the end of an indirect object
    ''' </summary>
    ''' <param name="indirectObject"></param>
    ''' <remarks></remarks>
    Private Sub _endWriteIndirectObject(ByVal indirectObject As clsPdfBaseObject)
        _write(" "c)
        _write(PDF_ENDOBJ)
        _write(ControlChars.CrLf)
        _objectsWritten.Add(indirectObject)
    End Sub
    ''' <summary>
    ''' Write a contiguous cross-reference table
    ''' </summary>
    ''' <param name="startIndex"></param>
    ''' <param name="endIndex"></param>
    ''' <remarks></remarks>
    Private Sub _writeXRef(ByVal startIndex As Integer, ByVal endIndex As Integer)
        Dim i As Integer
        _write(_objectsWritten(startIndex).ObjectNumber.ToString)
        _write(" "c)
        _write((_objectsWritten(endIndex).ObjectNumber - _objectsWritten(startIndex).ObjectNumber + 1).ToString)
        _write(ControlChars.CrLf)

        For i = startIndex To endIndex
            _write(_objectsWritten(i).Position.ToString.PadLeft(10, "0"c))
            _write(" "c)
            'Write(0.ToString.PadLeft(5, "0"c))
            _write(_objectsWritten(i).GenerationNumber.ToString.PadLeft(5, "0"c))
            _write(" n")
            _write(ControlChars.CrLf)
        Next
    End Sub
#End Region
End Class
